package com.smsc.headend.connector.bo;

import cn.hutool.json.JSONUtil;
import com.jcraft.jsch.*;
import com.smsc.headend.connector.service.FtpServerService;
import com.smsc.headend.module.data.dto.FtpCollectionDTO;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.springframework.util.CollectionUtils;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;


/**
 * @author liangli
 * @date 2020/12/1
 */
@Data
@Slf4j
public class RemoteDataCollector {
    public static final String FTP_FILE_REMOVE_PATH = "/tmp/udis/collection";

    public static final int SFTP_TYPE = 0;
    public static final int FTP_TYPE = 1;

    String host;
    Integer port;
    String userName;
    String password;

    List<String> collectedFilePath;
    Long ftpId;
    // 1- ftp 0-sftp
    Integer serverType;

    ChannelSftp channelSftp;

    FTPClient ftpClient;


    FtpServerService ftpServerService;

    public boolean connect() {
        Boolean connected = false;
        if (serverType == SFTP_TYPE) {
            connected = connectSftp();
        } else if (serverType == FTP_TYPE) {
            connected = connectFtp();
        }

        if (connected) {
            ftpServerService.updateLastConTv(this.ftpId);
        }

        return connected;
    }

    private Boolean connectFtp() {
        log.info("connectFtp...");
        if (ftpClient != null) {
            try {
                String status = ftpClient.getStatus();
                log.info("getStatus = {}", status);
                ftpClient.sendCommand("pwd");
                return true;
            } catch (Exception e) {
                log.error("connectAndSendTask ftp error", e);
                if (ftpClient.isConnected()) {
                    try {
                        ftpClient.disconnect();
                    } catch (IOException ioe) {
                        log.error("disconnect ftp error", e);
                    }
                }
                ftpClient = null;
                return false;
            }
        }

        try {
            ftpClient = new FTPClient();
            ftpClient.setConnectTimeout(30 * 1000);
            ftpClient.connect(host, port);
            ftpClient.login(userName, password);
            ftpClient.enterLocalPassiveMode();
            ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
            ftpClient.setDataTimeout(3 * 60 * 1000);
            ftpClient.setSoTimeout(3 * 60 * 1000);
            return true;
        } catch (IOException e) {
            log.error("get ftp connectAndSendTask failed", e);
            ftpClient = null;
        }
        return false;
    }


    private boolean connectSftp() {
        if (channelSftp != null && channelSftp.isConnected()) {
            return true;
        }
        log.info("connecting to {}@{}:{}", userName, host, port);
        JSch jsch = new JSch();
        Session session = null;
        try {
            session = jsch.getSession(userName, host, port);
            if (password != null) {
                session.setPassword(password);
            }
            Properties config = new Properties();
            config.put("StrictHostKeyChecking", "no");

            session.setConfig(config);
            session.connect();

            Channel channel = session.openChannel("sftp");
            channel.connect();
            channelSftp = (ChannelSftp) channel;
            return true;
        } catch (JSchException e) {
            log.error("get sftp connectAndSendTask failed");
        }
        return false;
    }

    public List<String> listCollectionFile(String path) {
        if (serverType == SFTP_TYPE) {
            return sftpList(path);
        }
        if (serverType == FTP_TYPE) {
            return ftpList(path);
        }
        return null;
    }

    private List<String> ftpList(String path) {
        try {
            List result = new ArrayList();
            FTPFile[] ftpFiles = ftpClient.listFiles(path);
            for (FTPFile ftpFile : ftpFiles) {
                String fileName = ftpFile.getName();
                if (fileName.endsWith(".gz") && ftpFile.getSize() > 0) {
                    result.add(fileName);
                }
            }
            return result;
        } catch (IOException e) {
            log.error("list ftp file failed {}", path, e);
        }
        return null;
    }

    private List<String> sftpList(String path) {
        try {
            List vector = channelSftp.ls(path);
            List result = new ArrayList();
            for (Object o : vector) {
                String fileName = ((ChannelSftp.LsEntry) o).getFilename();
                Long fileSize = ((ChannelSftp.LsEntry) o).getAttrs().getSize();
                if (fileName.endsWith(".gz") && fileSize > 0) {
                    result.add(fileName);
                }
            }
            return result;
        } catch (SftpException e) {
            log.error("get ftp server file fail", e);
        }
        return null;
    }

    public void collectData() {
        if (connect()) {
            log.info("collect: serverType={}, host={}:{}, path={}", this.serverType == 0 ? "sftp" : "ftp", this.host, this.port, JSONUtil.toJsonStr(this.collectedFilePath));
            for (String path : this.collectedFilePath) {
                log.debug("searching at  {}", path);
                List<String> fileNames = this.listCollectionFile(path);
                if (CollectionUtils.isEmpty(fileNames)) {
                    log.debug("no collection gz file");
                    continue;
                }
                downloadSendPersistFiles(fileNames, path, ftpServerService);
            }
        }
    }

    public void downloadSendPersistFiles(List<String> fileNames, String path, FtpServerService ftpServerService) {
        String oldPath = path.endsWith("/") ? path : path + "/";
        //获取文件流后解压缩
        if (serverType == SFTP_TYPE) {
            try {
                channelSftp.cd(path);
            } catch (SftpException e) {
                log.error("cd path error", e);
                return;
            }
            for (String fileName : fileNames) {
                Long fileSize = this.getFileSize(fileName, channelSftp);
                if (isFileWhole(fileName, fileSize)) {
                    log.info("download {}", fileName);
                    InputStream is = null;
                    try {
                        is = channelSftp.get(fileName);
                        byte[] fileData = IOUtils.toByteArray(is);
                        FtpCollectionDTO dto = decodeCollectionData(fileName, fileData);
                        ftpServerService.sendCollectionData(dto);
                        persistFile(fileName, fileData);
                        channelSftp.rm(fileName);
                    } catch (Exception e) {
                        log.error("download sftp file {} error", fileName, e);
                    } finally {
                        if (null != is) {
                            IOUtils.closeQuietly(is);
                        }
                    }
                }
            }
        }
        if (serverType == FTP_TYPE) {
            try {
                ftpClient.changeWorkingDirectory(path);
            } catch (IOException e) {
                log.error("change working directory error", e);
                return;
            }
            for (String fileName : fileNames) {
                Long fileSize = this.getFileSize(fileName, ftpClient);
                if (isFileWhole(fileName, fileSize)) {
                    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                    try {
                        ftpClient.retrieveFile(fileName, outputStream);
                        byte[] fileData = outputStream.toByteArray();
                        FtpCollectionDTO dto = decodeCollectionData(fileName, fileData);
                        ftpServerService.sendCollectionData(dto);
                        persistFile(fileName, fileData);
                        ftpClient.deleteFile(oldPath + fileName);
                    } catch (IOException e) {
                        log.error("download ftp file {} error", fileName, e);
                    } finally {
                        try {
                            if (outputStream != null) {
                                outputStream.close();
                            }
                        } catch (IOException e) {
                            log.error("close outputStream error", e);
                        }
                    }
                }
            }
        }
    }

    public boolean isExistDir(String path, ChannelSftp sftp) {
        boolean isExist = false;
        try {
            SftpATTRS sftpATTRS = sftp.lstat(path);
            isExist = true;
            return sftpATTRS.isDir();
        } catch (Exception e) {
            if (e.getMessage().toLowerCase().equals("no such file")) {
                isExist = false;
            }
        }
        return isExist;

    }

    /**
     * 保存文件到本地目录
     *
     * @param fileName
     * @param fileBytes
     */
    public void persistFile(String fileName, byte[] fileBytes) {
        ftpServerService.saveLocalCollectionFile(fileName, fileBytes);
    }

    public FtpCollectionDTO decodeCollectionData(String fileName, byte[] fileBytes) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        GZIPInputStream ungzip = new GZIPInputStream(new ByteArrayInputStream(fileBytes));
        byte[] buffer = new byte[256];
        int n;
        while ((n = ungzip.read(buffer)) >= 0) {
            out.write(buffer, 0, n);
        }
        byte[] unzipArray = out.toByteArray();
        String xml = new String(unzipArray, "ISO-8859-1");
        try {
            return translateDlmsGatewayFile(xml);
        } catch (DocumentException e) {
            log.error("parse xml fail {}", fileName);
        } finally {
            IOUtils.closeQuietly(ungzip);
        }
        return null;
    }


    private FtpCollectionDTO translateDlmsGatewayFile(String xml) throws DocumentException {
        Document document = DocumentHelper.parseText(xml);
        List<FtpCollectionDTO> list = new ArrayList<>();
        for (Element element : document.getRootElement().elements()) {
            if (element.getName().equalsIgnoreCase("Body")) {
                list.addAll(element.elements().stream().filter(el -> el.getName().equalsIgnoreCase("reportMeterData")).map(
                        this::xmlElementToDlmsGatewayCollectionData
                ).collect(Collectors.toList()));
            }
        }
        return list.get(0);
    }

    FtpCollectionDTO xmlElementToDlmsGatewayCollectionData(Element element) {
        FtpCollectionDTO ftpCollectionData = new FtpCollectionDTO();
        String dcuId = element.attribute("DcId").getValue();
        ftpCollectionData.setDcuSn(dcuId);
        for (Element taskElement : element.elements()) {
            FtpCollectionDTO.RecordWithPacket record = new FtpCollectionDTO.RecordWithPacket();
            String obis = taskElement.attribute("Obis").getValue();
            String meterId = taskElement.attribute("MeterId").getValue();
            String data = taskElement.attribute("Data").getValue();
            record.setObis(obis);
            record.setMeterSn(meterId);
            record.setMessage(data);
            ftpCollectionData.getRecords().add(record);
        }
        return ftpCollectionData;
    }

    /**
     * 获取文件大小(ChannelSftp)
     *
     * @param srcSftpFilePath
     * @param sftp
     * @return
     */
    public long getFileSize(String srcSftpFilePath, ChannelSftp sftp) {
        //文件大于等于0则存在
        long fileSize;
        try {
            SftpATTRS sftpATTRS = sftp.lstat(srcSftpFilePath);
            fileSize = sftpATTRS.getSize();
        } catch (Exception e) {
            //获取文件大小异常
            fileSize = -1;
            if (e.getMessage().toLowerCase().equals("no such file")) {
                //文件不存在
                fileSize = -2;
            }
        }
        return fileSize;
    }

    /**
     * 获取文件大小(FTPClient)
     *
     * @param srcSftpFilePath
     * @param ftpClient
     * @return
     */
    public long getFileSize(String srcSftpFilePath, FTPClient ftpClient) {
        //文件大于等于0则存在
        long fileSize = 0;
        try {
            String s = ftpClient.getSize(srcSftpFilePath);
            if (StringUtils.isNotEmpty(s)) {
                fileSize = Long.parseLong(ftpClient.getSize(srcSftpFilePath));
            }
        } catch (IOException e) {
            //获取文件大小异常
            fileSize = -1;
            if (e.getMessage().toLowerCase().equals("no such file")) {
                //文件不存在
                fileSize = -2;
            }
        }
        return fileSize;
    }

    /**
     * 判断文件是否完整(针对文件名有文件大小字段的，如：DCUSN_YYMMDD_HHMMSSMMM_372.gz)
     *
     * @return
     */
    public boolean isFileWhole(String fileName, Long fileSize) {
        String[] files1 = fileName.split("_");
        if (null != files1 && files1.length == 4) {
            String files2 = files1[3];
            String fileSizeStr = files2.split("\\.")[0];
            if (null != fileSizeStr && StringUtils.isNumeric(fileSizeStr)) {
                Long fileSizeInName = Long.valueOf(fileSizeStr);
                if (!fileSizeInName.equals(fileSize)) {
                    return false;
                }
            }
        }
        return true;
    }

    @Override
    public int hashCode() {
        return Objects.hash(ftpId);
    }

    public boolean equals(Object obj) {
        if (obj == null) return false;
        if (obj == this) return true;
        if (!(obj instanceof RemoteDataCollector)) return false;
        RemoteDataCollector o = (RemoteDataCollector) obj;
        return o.ftpId.compareTo(this.ftpId) == 0;
    }
}

